<?php
/**
 * @property string $content;
 */
class Response {
  private static $singleton_flag;
  private $status_map = array(
    200 => 'OK',

    301 => 'Moved Permanently',
    302 => 'Found',
    304 => 'Not Modified',

    400 => 'Bad Request',
    401 => 'Unauthorized',
    403 => 'Forbidden',
    404 => 'Not Found',
    405 => 'Method Not Allowed',
    406 => 'Not Acceptable',

    500 => 'Internal Server Error',
    501 => 'Not Implemented',
    503 => 'Service Unavailable',
  );

  private $content = '';
  private $headers = array();
  private $status;

  public function __construct() {
    if (isset(self::$singleton_flag) && !IS_CLI) {
      throw new Exception("Only allow single response instance in each request");
    }
    self::$singleton_flag = true;
    ob_start();
  }

  private function appendHeader($status) {
    if ($status) $this->status($status);
    //set default header
    if (!$this->status) $this->status(200);
    if (!isset($this->headers['Content-Type'])) $this->header('Content-Type', 'text/html; charset=utf-8');

    //send headers
    @header("HTTP/1.1 {$this->status}");
    foreach ($this->headers as $key => $value) {
      @header("{$key}: {$value}");
    }
  }

  private function read($content) {
    if (is_array($content)) $content = json_encode($content);

    if ($content) $this->replace($content);
    //set default error content.
    if (!$this->content && substr($this->status, 0, 3) !== '200') {
      $this->content = substr($this->status, 4);
    }
    $content = $this->content;
    return $content;
  }

  /**
   * send response, suggest do not call in controller.
   * @param string $content see as replace()
   * @param string $status see as status()
   */
  public function send($content = '', $status = '') {

    $this->appendHeader($status);
    ob_flush();

    //send content
    echo $this->read($content);

    //close connection
    //Storage::closeConnections();

    //should be end of request when response send.
    exit;
  }


  /**
   * @param string $content append content.
   * @return Response for chaining
   */
  public function append($content) {
    if ($content instanceof View) {
      $this->content .= $content->render();

    } else {
      $this->content .= $content;
    }

    return $this;
  }

  /**
   * @param string $content set new content.
   * @return Response for chaining
   */
  public function replace($content) {
    $this->content = $content;
    return $this;
  }

  /**
   * @param string $url, can be "back" as shortcut for the refer url.
   * @param int $status "302" by default, and can be set as "301"
   * @throws Exception when set wrong status code.
   */
  public function redirect($url, $status = 302) {
    if (!in_array($status, array(302, 301))) throw new Exception('Can not redirect with status:' . $status);
    $this->status($status);
    if ($url == 'back') $url = isset($_SERVER['HTTP_REFERER']) ? trim($_SERVER['HTTP_REFERER']) : '/';
    $this->header('Location', $url);
    $this->send("<a href='{$url}'>{$url}</a>");
  }

  /**
   * set status
   * @param int $status short for usual code "200", "404" etc.
   * @return Response for chaining
   * @throws Exception when set wrong status code (as not in $status_map).
   */
  public function status($status) {
    if (!isset($this->status_map[$status])) {
      throw new Exception("Unknown Status: {$status}");
    }
    $this->status = "{$status} {$this->status_map[$status]}";
    return $this;
  }

  /**
   * set header
   * @param $key "both key and value will be trim"
   * @param $value
   * @return Response for chaining
   * @throws Exception when any param contains new line.
   */
  public function header($key, $value) {
    if (preg_match("/[\n\r]/", $key . $value)) {
      throw new Exception('Header should not contains new line (\r,\n)');
    }
    //by default override the prev set.
    $this->headers[trim($key)] = trim($value);
    return $this;
  }

  /**
   * set the cache time of clint side (mostly browser)
   * @param: $seconds int for cache time. Set as "0" for no cache.
   * @return Response for chaining
   */
  public function setClientSideCache($seconds) {
    if (!$seconds) {
      $cache = 'private, max-age=0, no-cache, must-revalidate';
      $expires = -1;
    } else {
      $cache = 'max-age=' . $seconds;
      $expires = time() + $seconds;
    }
    $this->header("Cache-Control", $cache);
    $this->header("Expires", $expires);
    return $this;
  }
  public function alertErrorAndGoBack($msg) {
    $this->send("<script type='text/javascript'>alert('{$msg}');history.go(-1);</script>");
  }
  public function alertAndGoBack($msg) {
	$this->send("<script type='text/javascript'>alert('{$msg}');location.replace(document.referrer);</script>");
  }
  public function alertAndGoTo($msg, $url) {
    $this->send("<script type='text/javascript'>alert('{$msg}');location.replace('{$url}');</script>");
  }
  public function __get($name) {
    return $this->$name;
  }
}